package com.hqd.ch03.v46.boot.context.config;

import com.hqd.ch03.v46.core.env.PropertySource;

import java.util.*;
import java.util.function.Consumer;

public final class ConfigData {

    /**
     * A {@link ConfigData} instance that contains no data.
     */
    public static final ConfigData EMPTY = new ConfigData(Collections.emptySet());
    private final List<PropertySource<?>> propertySources;
    private final PropertySourceOptions propertySourceOptions;

    /**
     * Create a new {@link ConfigData} instance with the same options applied to each
     * source.
     *
     * @param propertySources the config data property sources in ascending priority
     *                        order.
     * @param options         the config data options applied to each source
     * @see #ConfigData(Collection, PropertySourceOptions)
     */
    public ConfigData(Collection<? extends PropertySource<?>> propertySources, Option... options) {
        this(propertySources, PropertySourceOptions.always(Options.of(options)));
    }

    /**
     * Create a new {@link ConfigData} instance with specific property source options.
     *
     * @param propertySources       the config data property sources in ascending priority
     *                              order.
     * @param propertySourceOptions the property source options
     * @since 2.4.5
     */
    public ConfigData(Collection<? extends PropertySource<?>> propertySources,
                      PropertySourceOptions propertySourceOptions) {
        this.propertySources = Collections.unmodifiableList(new ArrayList<>(propertySources));
        this.propertySourceOptions = propertySourceOptions;
    }

    /**
     * Return the configuration data property sources in ascending priority order. If the
     * same key is contained in more than one of the sources, then the later source will
     * win.
     *
     * @return the config data property sources
     */
    public List<PropertySource<?>> getPropertySources() {
        return this.propertySources;
    }

    /**
     * Return a set of {@link Option config data options} for this source.
     *
     * @return the config data options
     * @deprecated since 2.4.5 in favor of {@link #getOptions(PropertySource)}
     */
    @Deprecated
    public Set<Option> getOptions() {
        return this.propertySourceOptions.get(null).asSet();
    }

    /**
     * Return the {@link Options config data options} that apply to the given source.
     *
     * @param propertySource the property source to check
     * @return the options that apply
     * @since 2.4.5
     */
    public Options getOptions(PropertySource<?> propertySource) {
        Options options = this.propertySourceOptions.get(propertySource);
        return (options != null) ? options : Options.NONE;
    }

    /**
     * Option flags that can be applied.
     */
    public enum Option {

        /**
         * Ignore all imports properties from the source.
         */
        IGNORE_IMPORTS,

        /**
         * Ignore all profile activation and include properties.
         *
         * @since 2.4.3
         */
        IGNORE_PROFILES,

        /**
         * Indicates that the source is "profile specific" and should be included after
         * profile specific sibling imports.
         *
         * @since 2.4.5
         */
        PROFILE_SPECIFIC;

    }

    /**
     * Strategy interface used to supply {@link Options} for a given
     * {@link PropertySource}.
     *
     * @since 2.4.5
     */
    @FunctionalInterface
    public interface PropertySourceOptions {

        /**
         * {@link PropertySourceOptions} instance that always returns
         * {@link Options#NONE}.
         *
         * @since 2.4.6
         */
        PropertySourceOptions ALWAYS_NONE = new AlwaysPropertySourceOptions(Options.NONE);

        /**
         * Create a new {@link PropertySourceOptions} instance that always returns the
         * same options regardless of the property source.
         *
         * @param options the options to return
         * @return a new {@link PropertySourceOptions} instance
         */
        static PropertySourceOptions always(Option... options) {
            return always(Options.of(options));
        }

        /**
         * Create a new {@link PropertySourceOptions} instance that always returns the
         * same options regardless of the property source.
         *
         * @param options the options to return
         * @return a new {@link PropertySourceOptions} instance
         */
        static PropertySourceOptions always(Options options) {
            if (options == Options.NONE) {
                return ALWAYS_NONE;
            }
            return new AlwaysPropertySourceOptions(options);
        }

        /**
         * Return the options that should apply for the given property source.
         *
         * @param propertySource the property source
         * @return the options to apply
         */
        Options get(PropertySource<?> propertySource);

    }

    /**
     * {@link PropertySourceOptions} that always returns the same result.
     */
    private static class AlwaysPropertySourceOptions implements PropertySourceOptions {

        private final Options options;

        AlwaysPropertySourceOptions(Options options) {
            this.options = options;
        }

        @Override
        public Options get(PropertySource<?> propertySource) {
            return this.options;
        }

    }

    /**
     * A set of {@link Option} flags.
     *
     * @since 2.4.5
     */
    public static final class Options {

        /**
         * No options.
         */
        public static final Options NONE = new Options(Collections.emptySet());

        private final Set<Option> options;

        private Options(Set<Option> options) {
            this.options = Collections.unmodifiableSet(options);
        }

        /**
         * Create a new instance with the given {@link Option} values.
         *
         * @param options the options to include
         * @return a new {@link Options} instance
         */
        public static Options of(Option... options) {
            if (options.length == 0) {
                return NONE;
            }
            return new Options(EnumSet.copyOf(Arrays.asList(options)));
        }

        Set<Option> asSet() {
            return this.options;
        }

        /**
         * Returns if the given option is contained in this set.
         *
         * @param option the option to check
         * @return {@code true} of the option is present
         */
        public boolean contains(Option option) {
            return this.options.contains(option);
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null || getClass() != obj.getClass()) {
                return false;
            }
            Options other = (Options) obj;
            return this.options.equals(other.options);
        }

        @Override
        public int hashCode() {
            return this.options.hashCode();
        }

        @Override
        public String toString() {
            return this.options.toString();
        }

        /**
         * Create a new {@link Options} instance that contains the options in this set
         * excluding the given option.
         *
         * @param option the option to exclude
         * @return a new {@link Options} instance
         */
        public Options without(Option option) {
            return copy((options) -> options.remove(option));
        }

        /**
         * Create a new {@link Options} instance that contains the options in this set
         * including the given option.
         *
         * @param option the option to include
         * @return a new {@link Options} instance
         */
        public Options with(Option option) {
            return copy((options) -> options.add(option));
        }

        private Options copy(Consumer<EnumSet<Option>> processor) {
            EnumSet<Option> options = EnumSet.noneOf(Option.class);
            options.addAll(this.options);
            processor.accept(options);
            return new Options(options);
        }

    }

}
